namespace Microsoft.ServiceModel.Samples.Discovery
{
    using System;
    using System.Xml;
    using System.Threading;
    using System.Collections.ObjectModel;
    using System.ServiceModel;
    using System.ServiceModel.Description;
    using Microsoft.ServiceModel.Samples.Discovery.Protocol;
    
    public sealed class ServiceFinder : IDisposable, IFindResponseContract
    {
        bool disposed;
        OutputBinding outputBinding;
        DuplexChannelFactory<IFindContract> factory;
        IFindContract channel;
        FindCorrelator findCorrelator;
            
        public event EventHandler<AnnouncementEventArgs> ProxyAvailable;

        public ServiceFinder()
            : this(ProtocolUtility.CreateMulticastDuplexOutputBinding())
        {
        }

        public ServiceFinder(OutputBinding outputBinding)
        {
            Initialize(outputBinding);
        }

        void Initialize(OutputBinding outputBinding)
        {
            if (outputBinding == null)
                throw new ArgumentNullException("outputBinding");

            this.outputBinding = outputBinding;
            
            // create the channel factory for sending the find messages
            this.factory = new DuplexChannelFactory<IFindContract>(
                new InstanceContext(this),
                this.outputBinding.Binding,
                new EndpointAddress(this.outputBinding.ToUri));

            // add the via behaviour so that messages addresses to the 
            // well known discovery address can be send to appropriate 
            // physical address.
            this.factory.Endpoint.Behaviors.Add(
                new ClientViaBehavior(this.outputBinding.ViaUri));

            // create channel using which to send the probe.
            this.channel = this.factory.CreateChannel();

            this.findCorrelator = new FindCorrelator();
            this.disposed = false;
        }


        public ReadOnlyCollection<ServiceProperties> Probe(
            MatchCriteria criteria)
        {
            return Probe(criteria, DefaultDuration, Int32.MaxValue);
        }

        public ReadOnlyCollection<ServiceProperties> Probe(
            MatchCriteria criteria, TimeSpan duration, int maxResults)
        {
            FindAsyncResult asyncResult = (FindAsyncResult)BeginProbe(criteria, 
                duration, maxResults, null, null, null);
            asyncResult.End();

            return asyncResult.Context.Results.AsReadOnly();
        }

        public IAsyncResult BeginProbe(MatchCriteria criteria, 
            TimeSpan duration, int maxResults,
            IncrementalResultsCallback incrementalResultsCallback, 
            AsyncCallback asyncCallback, object asyncState)
        {
            if (criteria == null)
                throw new ArgumentNullException("criteria");

            if (duration <= TimeSpan.Zero)
                throw new ArgumentOutOfRangeException("duration");

            if (maxResults <= 0)
                throw new ArgumentOutOfRangeException("maxResults");

            // 1. Create new find context and async result.
            ProbeContext context = new ProbeContext(criteria, duration, 
                maxResults, incrementalResultsCallback);

            IAsyncResult asyncResult = new FindAsyncResult(context, 
                asyncCallback, asyncState);

            // 2. Start the find 
            StartFind(context);
    
            return asyncResult;
        }

        public ReadOnlyCollection<ServiceProperties> EndProbe(
            IAsyncResult asyncResult)
        {
            FindAsyncResult findAsyncResult = 
                FindAsyncResult.Validate(asyncResult);
            findAsyncResult.End();

            return findAsyncResult.Context.Results.AsReadOnly();
        }

        public void CancelProbe(IAsyncResult asyncResult)
        {
            FindAsyncResult findAsyncResult = 
                FindAsyncResult.Validate(asyncResult);
            CompleteFind(findAsyncResult.Context);
        }

        public ServiceProperties Resolve(EndpointAddress service)
        {
            return Resolve(service, ServiceFinder.DefaultDuration);
        }

        public ServiceProperties Resolve(EndpointAddress service, 
            TimeSpan duration)
        {
            FindAsyncResult asyncResult = (FindAsyncResult)BeginResolve(
                service, duration, null, null);
            asyncResult.End();

            if (asyncResult.Context.Results.Count > 0)
                return asyncResult.Context.Results[0];
            else
                return null;
        }

        public IAsyncResult BeginResolve(EndpointAddress service, 
            TimeSpan duration, AsyncCallback asyncCallback, object asyncState)
        {
            if (service == null)
                throw new ArgumentNullException("service");

            ResolveContext context = new ResolveContext(service, duration);
            IAsyncResult asyncResult = new FindAsyncResult(context, 
                asyncCallback, asyncState);
            StartFind(context);

            return asyncResult;
        }

        public void CancelResolve(IAsyncResult asyncResult)
        {
            FindAsyncResult findAsyncResult = 
                FindAsyncResult.Validate(asyncResult);
            CompleteFind(findAsyncResult.Context);
        }

        public ServiceProperties EndResolve(IAsyncResult asyncResult)
        {
            FindAsyncResult findAsyncResult = 
                FindAsyncResult.Validate(asyncResult);
            findAsyncResult.End();

            if (findAsyncResult.Context.Results.Count > 0)
                return findAsyncResult.Context.Results[0];
            else
                return null;
        }

        void StartFind(ProbeContext context)
        {
            try
            {
                // 1. create the find record.
                this.findCorrelator.AddFind(context);
                
                // 2. send the Message
                SendProbe(context);
                
                // 3. start the timer.
                context.StartTimer(new TimerCallback(this.TimerExpired));
            }
            catch
            {
                CompleteFind(context);
                throw;
            }
        }

        void StartFind(ResolveContext context)
        {
            try
            {
                // 1. create the find record.
                this.findCorrelator.AddFind(context);

                // 2. send the Message
                SendResolve(context);

                // 3. start the timer.
                context.StartTimer(new TimerCallback(this.TimerExpired));
            }
            catch
            {
                CompleteFind(context);
                throw;
            }
        }

        void CompleteFind(FindContext context, Exception error)
        {
            // if the find was active, complete it
            // otherwise ignore.
            if (this.findCorrelator.RemoveFind(context))
            {
                // dispose the timer. 
                context.StopTimer();
                context.AsyncResult.Complete(error);
            }
        }

        void CompleteFind(FindContext context)
        {
            CompleteFind(context, null);    
        }

        void SendProbe(ProbeContext context)
        {
            ProbeMessage message = new ProbeMessage();
            message.Probe = context.Probe;

            IClientChannel clientChannel = (IClientChannel)this.channel;
            using (new OperationContextScope(clientChannel))
            {
                OperationContext.Current.OutgoingMessageHeaders.ReplyTo = 
                    clientChannel.LocalAddress;

                OperationContext.Current.OutgoingMessageHeaders.MessageId = 
                    context.FindId;

                this.channel.ProbeOperation(message);
            }
        }

        void SendResolve(ResolveContext context)
        {
            ResolveMessage message = new ResolveMessage();
            message.Resolve = context.Resolve;

            IClientChannel clientChannel = (IClientChannel)this.channel;
            using (new OperationContextScope(clientChannel))
            {
                OperationContext.Current.OutgoingMessageHeaders.ReplyTo = 
                    clientChannel.LocalAddress;

                OperationContext.Current.OutgoingMessageHeaders.MessageId = 
                    context.FindId;

                this.channel.ResolveOperation(message);
            }
        }

        void TimerExpired(object state)
        {
            FindContext context = (FindContext)state;
            CompleteFind(context);
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (!this.disposed)
            {
                if (this.channel != null)
                    ((IClientChannel)this.channel).Dispose();

                if (this.factory != null)
                    this.factory.Close();

                this.disposed = true;
            }
        }

        #endregion

        public static readonly TimeSpan DefaultDuration = 
            TimeSpan.FromSeconds(20);
        
        #region FindResponseContract Members

        void IFindResponseContract.ProbeMatchOperation(ProbeMatchesMessage response)
        {
            // validate the message and drop the invalid message.
            FindContext findContext = ValidateResponseMessage();
            if (findContext == null)
            {
                return;
            }

            ProbeContext context = findContext as ProbeContext;
            if (context == null)
            {
                return;
            }

            if ((response.ProbeMatches == null) || 
                (response.ProbeMatches.Items == null))
            {
                return;
            }

            lock (context)
            {
                // has the find been cancelled or timer expired?
                if (!this.findCorrelator.IsActive(context)) return;
                
                foreach (ProbeMatch match in response.ProbeMatches.Items)
                {
                    if ((match == null) || (!match.IsValid()))
                    {
                        // the matched item is invalid
                        continue;
                    }

                    context.Results.Add(match);

                    if (context.IncrementalResultsCallback != null)
                    {
                        // the exceptions thrown from the callback will not be handled.
                        context.IncrementalResultsCallback(
                            context.AsyncResult, match);
                    }
                    
                    if (context.MaxResults == context.Results.Count)
                    {
                        // max results reached, complete the find.
                        CompleteFind(context);
                        break;
                    }
                }
            }    
        }

        void IFindResponseContract.ResolveMatchOperation(ResolveMatchesMessage response)
        {
            // validate the response message and drop the invalid message
            FindContext findContext = ValidateResponseMessage();
            if (findContext == null)
            {
                return;
            }

            ResolveContext context = findContext as ResolveContext;
            if (context == null)
            {
                return;
            }

            if ((response.ResolveMatches == null) || 
                (response.ResolveMatches.Items == null) || 
                (response.ResolveMatches.Items.Length > 1))
            {
                return;
            }

            lock (context)
            {
                // has the find been cancelled or timer expired?
                if (!this.findCorrelator.IsActive(context)) return;

                foreach (ResolveMatch match in response.ResolveMatches.Items)
                {
                    if ((match == null) || (!match.IsValid()))
                    {
                        // the matched item is invalid
                        continue;
                    }

                    context.Results.Add(match);

                    // we will hit this fater the first valid response
                    if (context.MaxResults == context.Results.Count)
                    {
                        // max results reached, complete the find.
                        CompleteFind(context);
                        break;
                    }
                }
            }
        }

        void IFindResponseContract.HelloOperation(HelloMessage message)
        {
            FindContext context = ValidateResponseMessage();
            if (context != null)
            {
                if (this.ProxyAvailable != null)
                {
                    this.ProxyAvailable(this, 
                        new AnnouncementEventArgs(
                            message.Hello, message.AppSequence));
                }
            }
        }

        FindContext ValidateResponseMessage()
        {
            UniqueId findId = 
                OperationContext.Current.IncomingMessageHeaders.RelatesTo;

            UniqueId msgId = 
                OperationContext.Current.IncomingMessageHeaders.MessageId;

            if ((findId == null) || (msgId == null))
            {
                // the message is invalid because it does not have required adderssing headers
                return null;
            }

            // find the context for this message.
            FindContext context = this.findCorrelator.LookupFind(findId);
            if (context == null)
            {
                // there are no active finds for this response 
                return null;
            }

            return context;
        }

        #endregion
    }
}
